<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <canvas id="myCanvas" width="1080" height="630" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    const ctx = myCanvas.getContext('2d');

    let p1 = [150, 300];
    let p2 = [850, 300];
    let c1 = [400, 150];
    let c2 = [600, 400];

    /**
 * 在一段三次贝塞尔曲线上均匀取点, 不包括终点
 * @param counts 一段贝塞尔曲线中取点的数量
 */
    getPntsOf3Bezier = function (p0, p1, p2, p3, counts) {
        let per = counts && counts != 0 ? 1 / counts : 0.02;    //取点间隔
        let points = [];
        for (let t = 0; t <= 0.999999; t += per) {
            points.push(getPntIn3Bezier(p0, p1, p2, p3, t));
        }
        return points;
    };

    /**
     * 获取三次贝塞尔曲线上的一点，t∈[0,1]
     * @param t 介于0 ~ 1, 表示点在曲线中的相对位置
     */
    getPntIn3Bezier = function (p0, p1, p2, p3, t) {
        let t_ = 1 - t;
        let x = p0[0] * t_ * t_ * t_ + 3 * p1[0] * t * t_ * t_ + 3 * p2[0] * t * t * t_ + p3[0] * t * t * t,
            y = p0[1] * t_ * t_ * t_ + 3 * p1[1] * t * t_ * t_ + 3 * p2[1] * t * t * t_ + p3[1] * t * t * t;
        return [x, y];
    };

    function getVctLen([x, y]) {
        return Math.sqrt(x * x + y * y);
    };

    /**
* 根据点在向量上的比例计算点坐标, [xO, yO]为起点，[xVct, yVct]为向量，k 为该点在向量方向上的长度
* 获取
*/
    function getPntInVct([xO, yO], [xVct, yVct], k) {
        let lenVct = getVctLen([xVct, yVct]);  // 获取向量长度
        let stdVct;
        if (lenVct === 0) {
            stdVct = [0, 0];
        } else {
            stdVct = [xVct / lenVct, yVct / lenVct];   // 单位向量
        }
        return [xO + k * stdVct[0], yO + k * stdVct[1]];
    };

    function getLenOfTwoPnts(p1, p2) {
        return Math.sqrt(Math.pow((p1[0] - p2[0]), 2) + Math.pow((p1[1] - p2[1]), 2));
    };

    vct = function (start, end) {
        let x = end[0] - start[0];
        let y = end[1] - start[1];
        return [x, y];
    };

    /**
 * 在曲线(多点折线, 按点顺序)上取长度为 len 的一点坐标, 包括点真实坐标和曲线点数组中仅次于该点的下标, 0 < k < 1;
 */
    calPntInCurveByLen = function (curvePnts, len) {
        let lenNodes = curvePnts.length;
        let P, index;
        let templen = 0, temp = 0;
        for (let j = 0; j < lenNodes - 1; j++) {
            temp = getLenOfTwoPnts(curvePnts[j], curvePnts[j + 1]);
            if (templen + temp > len) {
                P = getPntInVct(curvePnts[j], vct(curvePnts[j], curvePnts[j + 1]), len - templen);
                index = j;
                break;
            }
            templen += temp;
        }
        if (!P) {
            return null;
        }
        return [P, index];
    };


    /**
     * 计算折线的总长度
     */
    getLinStringLen = function (Nodes) {
        let len = 0;
        for (let i = 0; i < Nodes.length - 1; i++) {
            len += getLenOfTwoPnts(Nodes[i], Nodes[i + 1]);
        }
        return len;
    };

    /**
 * 在曲线(多点折线, 按点顺序)上取长度比为 k 的一点坐标, 包括点真实坐标和曲线点数组中仅次于该点的下标, 0 < k < 1;
 */
    calPntInCurveByK = function (curvePnts, lenCurve, k) {
        let lenNodes = curvePnts.length;
        let P, index;
        let templen = 0, temp = 0;
        for (let j = 0; j < lenNodes - 1; j++) {
            temp = getLenOfTwoPnts(curvePnts[j], curvePnts[j + 1]);
            if (templen + temp > k * lenCurve) {
                P = getPntInVct(curvePnts[j], vct(curvePnts[j], curvePnts[j + 1]), k * lenCurve - templen);
                index = j;
                break;
            }
            templen += temp;
        }
        return [P, index];
    };


    var len = .5;
    function draw() {
        ctx.font = "18px serif";
        ctx.lineWidth = 5;
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        let points = getPntsOf3Bezier(p1, c1, c2, p2, 100);
        points.forEach((p, i) => {
            if (i == 0) {
                ctx.moveTo(p[0], p[1]);
            } else {
                ctx.lineTo(p[0], p[1]);
            }
        })
        ctx.stroke();

        ctx.beginPath();
        ctx.fillStyle = "blue";
        ctx.arc(c1[0], c1[1], 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.fillText("控制点1", c1[0]+10, c1[1]);

        ctx.beginPath();
        ctx.fillStyle = "blue";
        ctx.arc(c2[0], c2[1], 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.fillText("控制点2", c2[0]+10, c2[1]);

        ctx.beginPath();
        let pLen = getLinStringLen(points);
        let gp2 = calPntInCurveByK(points, pLen, len)[0];
        ctx.fillStyle = "orange";
        ctx.arc(gp2[0], gp2[1], 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.fillText(`距离起点${len}的点坐标`, gp2[0]+10, gp2[1]);
    }

    setInterval(draw, 0);

    window.addEventListener("keydown", (e) => {
        if (e.keyCode == 38) {
            len += .1;
        }
        if (e.keyCode == 40) {
            len -= .1;
        }
    })

</script>
<style>
    canvas {
        border: 1px solid #ccc;
        background-image: linear-gradient(90deg,
                #ffffb8 1%,
                rgba(0, 0, 0, 0) 5%),
            linear-gradient(#ffffb8 1%, rgba(0, 0, 0, 0) 5%);
        background-size: 28px 28px;
    }
</style>